package com.clei.utils;

import com.clei.dto.db.ColumnProperty;
import com.clei.enums.DatabaseServerEnum;
import com.clei.utils.helper.DBHelper;

import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.stream.Collectors;

/**
 * 代码生成工具
 *
 * @author Y
 * @since 2024-02-02
 */
public final class CodeGenUtil {

    /**
     * dbServer
     */
    private final static DatabaseServerEnum DB_SERVER = DatabaseServerEnum.MYSQL;

    /**
     * dbHelper
     */
    private final static DBHelper DB_HELPER = DB_SERVER.getDbHelper();

    /**
     * mapper方法list
     */
    private static final List<String> MAPPER_METHOD_LIST = Arrays.asList("count", "list", "selectById", "insert",
            "insertBatch", "updateById", "updateByIdSelective", "deleteById");

    /**
     * author
     */
    private static final String DEFAULT_AUTHOR = "Y";

    /**
     * author
     */
    private static final String DEFAULT_EXPORT_PATH = "E:\\workTemp\\mybatis";

    /**
     * 参数key前缀
     */
    private static final char PARAM_FIRST = '_';

    /**
     * entity模版map
     */
    private static final Map<String, String> ENTITY_TEMP = new LinkedHashMap<>(3);

    /**
     * pageQuery模版map
     */
    private static final Map<String, String> PAGE_QUERY_TEMP = new LinkedHashMap<>(3);

    /**
     * addReq模版map
     */
    private static final Map<String, String> ADD_REQ_TEMP = new LinkedHashMap<>(3);

    /**
     * updateReq模版map
     */
    private static final Map<String, String> UPDATE_REQ_TEMP = new LinkedHashMap<>(3);

    /**
     * dto模版map
     */
    private static final Map<String, String> DTO_TEMP = new LinkedHashMap<>(3);

    /**
     * mapperJava模版map
     */
    private static final Map<String, String> MAPPER_TEMP = new LinkedHashMap<>(MAPPER_METHOD_LIST.size() + 2);

    /**
     * mapperXml模版map
     */
    private static final Map<String, String> MAPPER_XML_TEMP = new LinkedHashMap<>(MAPPER_METHOD_LIST.size() + 4);

    /**
     * 生成文件map
     */
    private static final Map<String, Map<String, String>> WRITE_FILE_MAP = new LinkedHashMap<>(7);

    /**
     * 生成文件map
     */
    private static final Map<String, String> FILE_SUFFIX_MAP = new HashMap<>(7);

    static {
        // write file map
        WRITE_FILE_MAP.put("_entity", ENTITY_TEMP);
        WRITE_FILE_MAP.put("_pageQuery", PAGE_QUERY_TEMP);
        WRITE_FILE_MAP.put("_addReq", ADD_REQ_TEMP);
        WRITE_FILE_MAP.put("_editReq", UPDATE_REQ_TEMP);
        WRITE_FILE_MAP.put("_dto", DTO_TEMP);
        WRITE_FILE_MAP.put("_mapperJava", MAPPER_TEMP);
        WRITE_FILE_MAP.put("_mapperXml", MAPPER_XML_TEMP);
        // file suffix map
        FILE_SUFFIX_MAP.put("_entity", "java");
        FILE_SUFFIX_MAP.put("_pageQuery", "java");
        FILE_SUFFIX_MAP.put("_addReq", "java");
        FILE_SUFFIX_MAP.put("_editReq", "java");
        FILE_SUFFIX_MAP.put("_dto", "java");
        FILE_SUFFIX_MAP.put("_mapperJava", "java");
        FILE_SUFFIX_MAP.put("_mapperXml", "xml");

        // entity
        ENTITY_TEMP.put("entityPrefix", "/**\n" +
                " * _entity entity\n" +
                " *\n" +
                " * @author _author\n" +
                " * @date _date\n" +
                " */\n" +
                "@Data\n" +
                "public class _entity {");
        ENTITY_TEMP.put("entityProperty", "_entityProperty");
        ENTITY_TEMP.put("entitySuffix", "}");
        // pageQuery
        PAGE_QUERY_TEMP.put("queryPrefix", "/**\n" +
                " * _entity pageQuery\n" +
                " *\n" +
                " * @author _author\n" +
                " * @date _date\n" +
                " */\n" +
                "@Data\n" +
                "@EqualsAndHashCode(callSuper = true)\n" +
                "public class _pageQuery extends PageQuery {");
        PAGE_QUERY_TEMP.put("queryProperty", "_entityProperty");
        PAGE_QUERY_TEMP.put("querySuffix", "}");
        // addReq
        ADD_REQ_TEMP.put("addReqPrefix", "/**\n" +
                " * _entity addReq\n" +
                " *\n" +
                " * @author _author\n" +
                " * @date _date\n" +
                " */\n" +
                "@Data\n" +
                "public class _addReq {");
        ADD_REQ_TEMP.put("addReqProperty", "_addProperty");
        ADD_REQ_TEMP.put("querySuffix", "}");
        // addReq
        UPDATE_REQ_TEMP.put("updateReqPrefix", "/**\n" +
                " * _entity updateReq\n" +
                " *\n" +
                " * @author _author\n" +
                " * @date _date\n" +
                " */\n" +
                "@Data\n" +
                "@EqualsAndHashCode(callSuper = true)\n" +
                "public class _editReq extends _addReq {");
        UPDATE_REQ_TEMP.put("updateReqProperty", "_updateProperty");
        UPDATE_REQ_TEMP.put("querySuffix", "}");
        // dto
        DTO_TEMP.put("dtoPrefix", "/**\n" +
                " * _entity dto\n" +
                " *\n" +
                " * @author _author\n" +
                " * @date _date\n" +
                " */\n" +
                "@Data\n" +
                "public class _dto {");
        DTO_TEMP.put("dtoProperty", "_entityProperty");
        DTO_TEMP.put("dtoSuffix", "}");
        // mapper java
        MAPPER_TEMP.put("mapperJavaPrefix", "/**\n" +
                " * _entity mapper\n" +
                " *\n" +
                " * @author _author\n" +
                " * @date _date\n" +
                " */\n" +
                "@Mapper\n" +
                "public interface _mapperJava {");
        MAPPER_TEMP.put("count", "/**\n" +
                "     * count\n" +
                "     *\n" +
                "     * @param query query\n" +
                "     * @return count\n" +
                "     */\n" +
                "    Integer count(_pageQuery query);");
        MAPPER_TEMP.put("list", "/**\n" +
                "     * list\n" +
                "     *\n" +
                "     * @param query query\n" +
                "     * @return list\n" +
                "     */\n" +
                "    List<_entity> list(_pageQuery query);");
        MAPPER_TEMP.put("selectById", "/**\n" +
                "     * selectById\n" +
                "     *\n" +
                "     * @param id id\n" +
                "     * @return _entity\n" +
                "     */\n" +
                "    _entity selectById(@Param(\"id\") Long id);");
        MAPPER_TEMP.put("insert", "/**\n" +
                "     * insert\n" +
                "     *\n" +
                "     * @param entity _entity\n" +
                "     * @return effect\n" +
                "     */\n" +
                "    int insert(_entity entity);");
        MAPPER_TEMP.put("insertBatch", "/**\n" +
                "     * insertBatch\n" +
                "     *\n" +
                "     * @param list entityList\n" +
                "     * @return effect\n" +
                "     */\n" +
                "    int insertBatch(@Param(\"list\") List<_entity> list);");
        MAPPER_TEMP.put("insertOrUpdateBatch", "/**\n" +
                "     * insertOrUpdateBatch\n" +
                "     *\n" +
                "     * @param list entityList\n" +
                "     * @return effect\n" +
                "     */\n" +
                "    int insertOrUpdateBatch(@Param(\"list\") List<_entity> list);");
        MAPPER_TEMP.put("updateById", "/**\n" +
                "     * updateById\n" +
                "     *\n" +
                "     * @param entity _entity\n" +
                "     * @return effect\n" +
                "     */\n" +
                "    int updateById(_entity entity);");
        MAPPER_TEMP.put("updateByIdSelective", "/**\n" +
                "     * updateByIdSelective\n" +
                "     *\n" +
                "     * @param entity _entity\n" +
                "     * @return effect\n" +
                "     */\n" +
                "    int updateByIdSelective(_entity entity);");
        MAPPER_TEMP.put("deleteById", "/**\n" +
                "     * deleteById\n" +
                "     *\n" +
                "     * @param idList idList\n" +
                "     * @return effect\n" +
                "     */\n" +
                "    int deleteById(@Param(\"idList\") List<Long> idList);");
        MAPPER_TEMP.put("mapperJavaSuffix", "}");

        // mapper xml
        MAPPER_XML_TEMP.put("mapperPrefix", "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
                "<!DOCTYPE mapper PUBLIC \"-//mybatis.org//DTD Mapper 3.0//EN\" \"http://mybatis.org/dtd/mybatis-3-mapper.dtd\">\n");
        MAPPER_XML_TEMP.put("mapperHead", "<mapper namespace=\"_packageMapper\">");
        MAPPER_XML_TEMP.put("BaseMap", "<resultMap id=\"BaseMap\" type=\"_packageEntity\">\n" +
                "        _resultMap" +
                "    </resultMap>\n");
        MAPPER_XML_TEMP.put("BaseColumn", "<sql id=\"BaseColumn\">\n" +
                "        _columnList\n" +
                "    </sql>\n");
        MAPPER_XML_TEMP.put("count", "<select id=\"count\" parameterType=\"_packageQuery\" resultType=\"java.lang.Integer\">\n" +
                "        SELECT COUNT(1)\n" +
                "        FROM _table\n" +
                "        <where>\n" +
                "            _ifEqual" +
                "        </where>\n" +
                "    </select>\n");
        MAPPER_XML_TEMP.put("list", "<select id=\"list\" parameterType=\"_packageQuery\" resultMap=\"BaseMap\">\n" +
                "        SELECT\n" +
                "        <include refid=\"BaseColumn\"/>\n" +
                "        FROM _table\n" +
                "        <where>\n" +
                "            _ifEqual" +
                "        </where>\n" +
                "        ORDER BY id DESC\n" +
                "        <if test=\"limit != null and offset != null\">\n" +
                "            LIMIT #{limit} OFFSET #{offset}\n" +
                "        </if>\n" +
                "    </select>\n");
        MAPPER_XML_TEMP.put("selectById", "<select id=\"selectById\" parameterType=\"java.lang.Long\" resultMap=\"BaseMap\">\n" +
                "        SELECT\n" +
                "        <include refid=\"BaseColumn\"/>\n" +
                "        FROM _table\n" +
                "        WHERE id = #{id}\n" +
                "    </select>\n");
        MAPPER_XML_TEMP.put("insert", "<insert id=\"insert\" parameterType=\"_packageEntity\" useGeneratedKeys=\"true\"\n" +
                "            keyProperty=\"id\">\n" +
                "        INSERT INTO _table(_columnListNoId)\n" +
                "        VALUES (_valueListNoId)\n" +
                "    </insert>\n");
        MAPPER_XML_TEMP.put("insertBatch", "<insert id=\"insertBatch\" parameterType=\"java.util.List\" useGeneratedKeys=\"true\" keyProperty=\"id\">\n" +
                "        INSERT INTO _table(_columnListNoId)\n" +
                "        VALUES\n" +
                "        <foreach collection=\"list\" item=\"item\" separator=\"),(\" open=\"(\" close=\")\">\n" +
                "            _itemValueListNoId\n" +
                "        </foreach>\n" +
                "    </insert>\n");
        MAPPER_XML_TEMP.put("insertOrUpdateBatch", "<insert id=\"insertOrUpdateBatch\" parameterType=\"java.util.List\">\n" +
                "        INSERT INTO _table(_columnList)\n" +
                "        VALUES\n" +
                "        <foreach collection=\"list\" item=\"item\" separator=\"),(\" open=\"(\" close=\")\">\n" +
                "            _itemValueList\n" +
                "        </foreach>\n" +
                "        ON DUPLICATE KEY UPDATE\n" +
                "        _duplicateUpdateColumnList" +
                "    </insert>\n");
        MAPPER_XML_TEMP.put("updateById", "<update id=\"updateById\" parameterType=\"_packageEntity\">\n" +
                "        UPDATE _table\n" +
                "        <set>\n" +
                "            _setColumnNoId" +
                "        </set>\n" +
                "        WHERE id = #{id}\n" +
                "    </update>\n");
        MAPPER_XML_TEMP.put("updateByIdSelective", "<update id=\"updateByIdSelective\" parameterType=\"_packageEntity\">\n" +
                "        UPDATE _table\n" +
                "        <set>\n" +
                "            _ifSetColumnNoId" +
                "        </set>\n" +
                "        WHERE id = #{id}\n" +
                "    </update>\n");
        MAPPER_XML_TEMP.put("deleteById", "<delete id=\"deleteById\" parameterType=\"java.util.List\">\n" +
                "        DELETE FROM _table\n" +
                "        WHERE id IN\n" +
                "        <foreach collection=\"idList\" item=\"item\" separator=\",\" open=\"(\" close=\")\">#{item}</foreach>\n" +
                "    </delete>\n");
        MAPPER_XML_TEMP.put("mapperTail", "</mapper>");
    }

    public static void main(String[] args) {
        generate();
    }

    /**
     * 生成
     */
    private static void generate() {
        // 手动选择
        Scanner input = new Scanner(System.in);
        PrintUtil.print("数据库* : ");
        String database = input.nextLine();
        PrintUtil.print("表* (多个以英文逗号分隔): ");
        String table = input.nextLine();
        PrintUtil.print("包* : ");
        String packageName = input.nextLine();
        PrintUtil.print("导出路径* : ");
        String exportPath = input.nextLine();
        PrintUtil.print("作者 : ");
        String author = input.nextLine();
        if (StringUtil.isBlank(exportPath)) {
            exportPath = DEFAULT_EXPORT_PATH;
        }
        if (StringUtil.isBlank(author)) {
            author = DEFAULT_AUTHOR;
        }
        if (StringUtil.isBlank(database) || StringUtil.isBlank(table) || StringUtil.isBlank(packageName)) {
            PrintUtil.log("数据库、表、包、导出路径必填！");
        }
        String[] tableArr = table.split(",");
        for (String tab : tableArr) {
            generate(database, tab, packageName, author, exportPath);
        }
    }

    /**
     * 生成
     *
     * @param database    数据库
     * @param table       表
     * @param packageName 包
     * @param author      作者
     * @param exportPath  导出路径
     */
    private static void generate(String database, String table, String packageName, String author, String exportPath) {
        List<ColumnProperty> columnPropertyList = getColumnPropertyList(database, table);
        Map<String, String> paramMap = getParamMap(packageName, table, author, columnPropertyList);
        WRITE_FILE_MAP.forEach((k, v) -> {
            String fileName = paramMap.get(k);
            String fileSuffix = FILE_SUFFIX_MAP.get(k);
            List<String> content = v.values().stream()
                    .map(s -> format(s, paramMap)).collect(Collectors.toList());
            FileUtil.writeTxt(exportPath + File.separator + fileName + '.' + fileSuffix, content);
        });
    }

    /**
     * getColumnPropertyList
     *
     * @param database 数据库
     * @param table    表
     * @return list
     */
    private static List<ColumnProperty> getColumnPropertyList(String database, String table) {
        List<Map<String, String>> columnList = MybatisUtil.commonSingleOperation(mapper -> mapper.selectMysqlColumn(database, table));
        return ColumnProperty.columnPropertyList(columnList, DB_HELPER::getJavaType);
    }

    /**
     * getParamMap
     *
     * @param packageName        包
     * @param table              表
     * @param author             作者
     * @param columnPropertyList 列属性list
     * @return paramMap
     */
    private static Map<String, String> getParamMap(String packageName, String table, String author, List<ColumnProperty> columnPropertyList) {
        int columnCount = columnPropertyList.size();
        String className = StringUtil.snake2Camel(table, true);
        String mapperName = className + "Mapper";
        Map<String, String> ifTempMap = getMapperXmlIfTemp(columnPropertyList);
        Map<String, String> paramMap = new HashMap<>();
        paramMap.put("_author", author);
        paramMap.put("_date", DateUtil.currentDate());
        paramMap.put("_mapperJava", mapperName);
        paramMap.put("_mapperXml", mapperName);
        paramMap.put("_entity", className);
        String pageQueryName = className + "PageQuery";
        paramMap.put("_pageQuery", pageQueryName);
        paramMap.put("_addReq", className + "AddReq");
        paramMap.put("_editReq", className + "EditReq");
        paramMap.put("_dto", className + "DTO");
        paramMap.put("_packageMapper", packageName + ".mapper." + mapperName);
        paramMap.put("_packageEntity", packageName + ".entity." + className);
        paramMap.put("_packageQuery", packageName + ".dto.req." + pageQueryName);
        // resultColumn
        List<String> entityPropertyList = new ArrayList<>(columnCount);
        List<String> addReqPropertyList = new ArrayList<>(columnCount);
        List<String> propertyNameList = new ArrayList<>(columnCount);
        List<String> columnNameList = new ArrayList<>(columnCount);
        List<String> resultMapList = new ArrayList<>(columnCount);
        List<String> ifEuqalList = new ArrayList<>(columnCount);
        List<String> setColumnList = new ArrayList<>(columnCount);
        List<String> setColumnNoIdList = new ArrayList<>(columnCount);
        List<String> ifSetColumnNoIdList = new ArrayList<>(columnCount);
        String updateReqProperty = null;
        String resultMapTemp = "<%s property=\"%s\" column=\"%s\" javaType=\"%s\"/>";
        String propertyTemp = "/**\n     * %s\n     */\n    private %s %s;";
        for (int i = 0; i < columnCount; i++) {
            ColumnProperty cp = columnPropertyList.get(i);
            String property = String.format(propertyTemp, cp.getColumnComment(), cp.getPropertySimpleType(), cp.getPropertyName());
            String setColumn = String.format("%s = #{%s},", cp.getColumnName(), cp.getPropertyName());
            // 最后一个 去掉逗号
            if (i == columnCount - 1) {
                setColumn = setColumn.substring(0, setColumn.length() - 1);
            }
            if (!"id".equals(cp.getPropertyName())) {
                resultMapList.add(String.format(resultMapTemp, "result", cp.getPropertyName(), cp.getColumnName(), cp.getPropertyType()));
                addReqPropertyList.add(property);
                setColumnNoIdList.add(setColumn);
                ifSetColumnNoIdList.add(String.format(ifTempMap.get(cp.getPropertyName()), setColumn));
            } else {
                resultMapList.add(String.format(resultMapTemp, "id", cp.getPropertyName(), cp.getColumnName(), cp.getPropertyType()));
                updateReqProperty = property;
            }
            propertyNameList.add(cp.getPropertyName());
            setColumnList.add(setColumn);
            entityPropertyList.add(property);
            columnNameList.add(cp.getColumnName());
            ifEuqalList.add(String.format(ifTempMap.get(cp.getPropertyName()),
                    String.format("AND %s = #{%s}", cp.getColumnName(), cp.getPropertyName())));
        }
        paramMap.put("_resultMap", String.join("\n", resultMapList));
        paramMap.put("_columnList", String.join(",", columnNameList));
        columnNameList.remove("id");
        paramMap.put("_columnListNoId", String.join(",", columnNameList));
        paramMap.put("_itemValueList", propertyNameList.stream().map(pn -> "#{item." + pn + "}").collect(Collectors.joining(",")));
        paramMap.put("_duplicateUpdateColumnList", columnNameList.stream().map(cn -> String.format("%s = VALUES(%s)", cn, cn)).collect(Collectors.joining(",\n")));
        propertyNameList.remove("id");
        paramMap.put("_valueListNoId", propertyNameList.stream().map(pn -> "#{" + pn + "}").collect(Collectors.joining(",")));
        paramMap.put("_itemValueListNoId", propertyNameList.stream().map(pn -> "#{item." + pn + "}").collect(Collectors.joining(",")));
        paramMap.put("_table", table);
        paramMap.put("_ifEqual", String.join("", ifEuqalList));
        paramMap.put("_setColumn", String.join("\n", setColumnList));
        paramMap.put("_setColumnNoId", String.join("\n", setColumnNoIdList));
        paramMap.put("_ifSetColumnNoId", String.join("", ifSetColumnNoIdList));
        paramMap.put("_entityProperty", String.join("\n", entityPropertyList));
        paramMap.put("_addProperty", String.join("\n", addReqPropertyList));
        paramMap.put("_updateProperty", updateReqProperty);
        return paramMap;
    }

    /**
     * 解析并填充参数
     *
     * @param str      模版字符串
     * @param paramMap 参数map
     */
    private static String format(String str, Map<String, String> paramMap) {
        StringBuilder formatted = new StringBuilder();
        char[] arr = str.toCharArray();
        int length = arr.length;
        boolean paramStart = false;
        int start = 0;
        for (int i = 0; i < length; i++) {
            char c = arr[i];
            if (PARAM_FIRST == c) {
                paramStart = true;
                start = i;
            } else if (paramStart) {
                if (!StringUtil.isLetter(c)) {
                    String paramKey = str.substring(start, i);
                    String paramValue = paramMap.get(paramKey);
                    formatted.append(paramValue);
                    formatted.append(c);
                    paramStart = false;
                }
            } else {
                formatted.append(c);
            }
        }
        // 最后直接是paramKey
        if (paramStart) {
            String paramKey = str.substring(start);
            String paramValue = paramMap.get(paramKey);
            formatted.append(paramValue);
        }
        return formatted.toString();
    }

    /**
     * getMapperXmlIfTemp
     *
     * @param columnPropertyList 列属性list
     * @return ifTestTempMap
     */
    private static Map<String, String> getMapperXmlIfTemp(List<ColumnProperty> columnPropertyList) {
        Map<String, String> ifTempMap = new HashMap<>(columnPropertyList.size());
        for (ColumnProperty cp : columnPropertyList) {
            String ifPrefix;
            if ("String".equals(cp.getPropertySimpleType())) {
                ifPrefix = String.format("<if test=\"%s != null and %s != ''\">\n", cp.getPropertyName(), cp.getPropertyName());
            } else {
                ifPrefix = String.format("<if test=\"%s != null\">\n", cp.getPropertyName());
            }
            ifTempMap.put(cp.getPropertyName(), ifPrefix + "%s\n</if>\n");
        }
        return ifTempMap;
    }
}